﻿package org.molamil.acto.pages {
	import org.molamil.acto.core.events.ActoErrorEvent;
	import org.molamil.acto.core.events.ActoEvent;
	import org.molamil.acto.core.loggers.Logger;
	import org.molamil.acto.core.roles.RoleEvent;
	import org.molamil.acto.core.session.ActoSession;
	import org.molamil.acto.handlers.AbstractHandlersController;
	import org.molamil.acto.handlers.HandlerContext;
	import org.molamil.acto.handlers.HandlersController;
	import org.molamil.acto.utils.EventUtils;
	import org.molamil.acto.utils.ObjectUtils;

	import flash.errors.IllegalOperationError;
	import flash.utils.getQualifiedClassName;

	/**
	 * XXX: Document.
	 * 
	 * @author Jorge Hernandez
	 */
	public class AbstractPagesController extends AbstractHandlersController implements PagesController, HandlersController {


		// CONSTANTS
		
		private static const log:Logger = Logger.getLogger("AbstractPagesController");
		
		public static const PARAM_NAME_IS_RELOAD:String = "_isReload";
		public static const PARAM_NAME_CALLER:String = "_caller";
		
		public static const ACTION_GOTO:String = "goto";
		public static const ACTION_DISPLAY_BLOCK:String = "displayBlock";
		public static const ACTION_CLEAR_BLOCK:String = "clearBlock";
		
		public static const HANDLER_PRIORITY_HIGH:int = 1;
		
		private static const ERROR_PAGE_NOT_FOUND:String = "ERROR: Page not found: \"{page}\".";
		private static const ERROR_MOVIE_NOT_FOUND:String = "ERROR: Block not found: \"{block}\".";
		private static const INVALID_PAGE_ROLE:String = "Invalid role for page \"{page}\": required \"{required}\" but found \"{current}\".";
		private static const INVALID_BLOCK_ROLE:String = "Invalid role for block \"{block}\": required \"{required}\" but found \"{current}\".";
		private static const INVALID_ACTION:String = "Invalid action \"{action}\" for page/block \"{id}\", the action will not be executed.";

		
		// PROPERTIES
		
		protected var currentPage:Page;
		protected var displayedPage:Page;
		protected var nextPage:Page;
		protected var displayedBlocks:Array; // of Block
		protected var pagesSession:PagesSession;
		
		private var _factory:AbstractPageBlockFactory;

		
		// CONSTRUCTOR
		
		public function AbstractPagesController(session:ActoSession) {
			
            super(session);
            
			// Abstract check.
			if (getQualifiedClassName(this) == "org.molamil.acto.pages::AbstractPagesController")
            	throw new IllegalOperationError("AbstractPagesController must be extended");
            	
			pagesSession = PagesSession(session);
            displayedBlocks = new Array();
            
            // Create empty factory, this will likely be overriden.
            factory = new SimplePageBlockFactory();
            
		}

		
		// PUBLIC METHODS
		
		public function goto(id:String, params:Object = null):Page {
			
			log.info("goto(), id = " + id);
				
			if (params == null)
				params = new Object();
			
			if (currentPage != null) {
				log.debug("currentPage id = " + currentPage.context.id);
			} else {
				log.debug("currentPage is null");
			}
			
			// Look up the page context.
			var pageContext:PageContext = factory.getPage(id);
			
			// Check if it's an alias page.
			if (pageContext != null) {
				if (pageContext.alias != null) {
					log.info("alias page: " + pageContext.alias);
					return goto(pageContext.alias, params);
				}
			}
			
			// Create the master.
			var page:Page;
			
			// Check whether we are already in that page.
			var isCurrent:Boolean = false;
			if (currentPage != null) {
				if (id == currentPage.context.id && params[PARAM_NAME_IS_RELOAD] != true) {
					log.info("No page to goto - we are already in " + id);
					isCurrent = true;
				}
			}
			
			// And wether that page is already being requested.
			var isRequested:Boolean = false;
			if (nextPage != null) {
				if (id == nextPage.context.id) {
					log.info("no page to goto - we the page is already being requested " + id);
					isRequested = true;
				}
			}
			
			if (!isCurrent && !isRequested) {
				
				// Create the request.
				var request:PageRequest = new PageRequest();
				request.id = id;
				request.params = params;
				request.referrer = currentPage;
				
				// Throw page request event.
				dispatchEvent(new PagesEvent(PagesEvent.PAGE_REQUEST, false, false, null, pageContext, request));
				
				// Get the requested context.
				if (pageContext == null) {
					var pageNotFoundMessage:String = ERROR_PAGE_NOT_FOUND.replace("{page}", id);
					log.severe(pageNotFoundMessage);
					dispatchEvent(new ActoErrorEvent(PagesEvent.PAGE_NOT_FOUND, false, false, null, pageNotFoundMessage, 
													 new PageNotFoundError(pageNotFoundMessage)));
					return null;
				}
				
				// Check the role.
				if (pageContext.role != null && pageContext.role != "" && pagesSession.roleProvider != null && !pagesSession.roleProvider.validate(pageContext.role)) {
					log.warning(INVALID_PAGE_ROLE.replace("{page}", id).replace("{required}", pageContext.role).replace("{current}", 
								pagesSession.roleProvider.currentRole));
					dispatchEvent(new RoleEvent(RoleEvent.REQUIRED, false, false, id, pagesSession.roleProvider.currentRole, request));
					return null;
				}
				
				// Build the page
				page = PageBlockBuilder.createPage(pageContext, request, pagesSession);
				
				// Set the current page.
				currentPage = page;
				
				// Add the depends.
				for (var i:Number = 0; i < pageContext.depends.length; i++)
					page.addDepends(pageContext.depends[i]);
					
				// Add the listeners to the requested page.
				EventUtils.propagate(page.master, this, PagesEvent.PAGE_DISPLAY);
				EventUtils.propagate(page.master, this, PagesEvent.PAGE_CLEAR);
				EventUtils.propagate(page.master, this, PagesEvent.PAGE_INIT);
				EventUtils.propagate(page.master, this, PagesEvent.PAGE_IN);
				EventUtils.propagate(page.master, this, PagesEvent.PAGE_ON);
				EventUtils.propagate(page.master, this, PagesEvent.PAGE_OUT);
				EventUtils.propagate(page.master, this, PagesEvent.PAGE_OFF);
				EventUtils.propagate(page.master, this, PagesEvent.PAGE_LOAD_START);
				EventUtils.propagate(page.master, this, PagesEvent.PAGE_LOAD_PROGRESS);
				EventUtils.propagate(page.master, this, PagesEvent.PAGE_LOAD_STATUS);
				EventUtils.propagate(page.master, this, PagesEvent.PAGE_LOAD_COMPLETE);
				EventUtils.propagate(page.master, this, PagesEvent.PAGE_LOAD_ERROR);
				EventUtils.propagate(page.master, this, PagesEvent.PAGE_UNLOAD);
				page.master.addEventListener(PagesEvent.PAGE_LOAD_ERROR, pageLoadErrorHandler);
				page.master.addEventListener(PagesEvent.PAGE_OFF, pageOffHandler);
				page.master.addEventListener(ActoEvent.TARGET_CREATED, targetCreatedHandler);
				page.master.addEventListener(PagesEvent.PAGE_CLEAR, clearHandler);
								
				//FIXME: Test when isDisplayAtOff = false since if you go out of a page and in again before it has been removed
				//		 it will be buggy. Also bugs spotted when rapid request. Test all this with different state managers.
				// Show and register the requested page.
				if (displayedPage != null && displayedPage.master.isDisplayed && pagesSession.isDisplayAtOff) {
					log.debug("Displaying at off");
					nextPage = page;
					if (displayedPage != null)
						displayedPage.master.clear();
				} else {
					if (displayedPage != null)
						displayedPage.master.clear();
					page.master.display();
					displayedPage = page;
					displayDepends(page);
					nextPage = null;
				}
				
				// Check if it's a redirect page.
				if (pageContext.redirect != null) {
					log.info("redirect page: " + pageContext.alias);
					goto(pageContext.redirect, params);
				}
				
			}
			
			return page;
		}

		public function reloadPage(params:Object = null):Page {
			
			//FIXME: The depends look buggy.
			
			var id:String = currentPage.context.id;
			var currentParams:Object = currentPage.request.params;
			
			if (currentParams == null)
				currentParams = new Object();
			ObjectUtils.merge(currentParams, params);
			currentParams[PARAM_NAME_IS_RELOAD] = true;
			
			log.info("reloadPage(), id = " + id);
			
			return goto(id, currentParams);
			
		}
		
		public function getDisplayedPage():Page {
			
			var pageId:String;
			if (displayedPage != null)
				pageId = displayedPage.context.id;
			
			log.debug("getDisplayedPage(): " + pageId);
			
			return displayedPage;
			
		}
		
		public function getCurrentPage():Page {
			
			if (currentPage == null) {
				log.info("getCurrentPage() returning null - there is no currentPage");
				return null;
			}
			
			log.debug("getCurrentPage(): " + currentPage.context.id);
			
			return currentPage;
			
		}
		
		public function displayBlock(id:String, params:Object = null):Block {
			
			log.info("displayBlock(), id = " + id);
			
			if (params == null)
				params = new Object();
		
			// Get the block if displayed.
			var block:Block = getDisplayedBlock(id);
			
			// Check whether it's already displayed.
			if (block != null) {
				
				//XXX: Review this logic.
				// If it's a depends movie, add the caller.
				if (params[PARAM_NAME_CALLER] != null) {
					block.addCaller(params[PARAM_NAME_CALLER]);
				} else {
					block.addCaller(block.context.id);
				}
				
				log.info("No block to display - the block is already displayed: " + id);
				
			} else {
			
				// Get the block context.
				var blockContext:BlockContext = factory.getBlock(id);
				
				// Throw request event.
				dispatchEvent(new PagesEvent(PagesEvent.BLOCK_REQUEST, false, false, null, blockContext));
					
				// Get the requested context.
				if (blockContext == null) {
					var blockNotFoundMessage:String = ERROR_MOVIE_NOT_FOUND.replace("{block}", id);
					log.severe(blockNotFoundMessage);
					dispatchEvent(new ActoErrorEvent(PagesEvent.BLOCK_NOT_FOUND, false, false, null, blockNotFoundMessage, 
													 new BlockNotFoundError(blockNotFoundMessage)));
					return null;
				}
						
				// Create the request.
				var request:BlockRequest = new BlockRequest();
				request.id = id;
				request.params = params;
				
				// Check the role.
				if (blockContext.role != null && blockContext.role != "" && pagesSession.roleProvider != null && !pagesSession.roleProvider.validate(blockContext.role)) {
					log.warning(INVALID_BLOCK_ROLE.replace("{block}", id).replace("{required}", blockContext.role).replace("{current}", 
								pagesSession.roleProvider.currentRole));
					dispatchEvent(new RoleEvent(RoleEvent.REQUIRED, false, false, id, pagesSession.roleProvider.currentRole, request));
					return null;
				}
				
				// Build the movie
				block = PageBlockBuilder.createBlock(blockContext, request, pagesSession);
				
				// Add the depends.
				for (var i:Number = 0; i < blockContext.depends.length; i++)
					block.addDepends(blockContext.depends[i]);
				
				// Add the caller.
				if (params[PARAM_NAME_CALLER] != null) {
					block.request.caller = params[PARAM_NAME_CALLER];
				} else {
					block.request.caller = block.context.id;
				}
				block.addCaller(block.request.caller);
				
				// Add the listeners to the requested block.
				EventUtils.propagate(block.master, this, PagesEvent.BLOCK_DISPLAY);
				EventUtils.propagate(block.master, this, PagesEvent.BLOCK_CLEAR);
				EventUtils.propagate(block.master, this, PagesEvent.BLOCK_INIT);
				EventUtils.propagate(block.master, this, PagesEvent.BLOCK_IN);
				EventUtils.propagate(block.master, this, PagesEvent.BLOCK_ON);
				EventUtils.propagate(block.master, this, PagesEvent.BLOCK_OUT);
				EventUtils.propagate(block.master, this, PagesEvent.BLOCK_OFF);
				EventUtils.propagate(block.master, this, PagesEvent.BLOCK_LOAD_START);
				EventUtils.propagate(block.master, this, PagesEvent.BLOCK_LOAD_PROGRESS);
				EventUtils.propagate(block.master, this, PagesEvent.BLOCK_LOAD_STATUS);
				EventUtils.propagate(block.master, this, PagesEvent.BLOCK_LOAD_COMPLETE);
				EventUtils.propagate(block.master, this, PagesEvent.BLOCK_LOAD_ERROR);
				EventUtils.propagate(block.master, this, PagesEvent.BLOCK_UNLOAD);
				block.master.addEventListener(PagesEvent.BLOCK_OFF, blockOffHandler);
				block.master.addEventListener(ActoEvent.TARGET_CREATED, targetCreatedHandler);
				block.master.addEventListener(PagesEvent.BLOCK_CLEAR, clearHandler);
					
				// Add the movie to the displayed list.
				addDisplayedBlock(block);
				
				// Show the requested block.
				try {
					block.master.display();
				} catch (e:Error) {
					removeDisplayedBlock(id);
					throw e;
				}
				
				// Show depends movies.
				displayDepends(block);
				
			}
			
			return block;
			
		}
		
		public function displayBlockForPage(id:String, params:Object = null):Block {
			
			log.info("displayBlockForPage(), id = " + id);
		
			if (params == null)
				params = new Object();
			params[PARAM_NAME_CALLER] = displayedPage.context.id;
			
			displayedPage.addDepends(id);
				
			return displayBlock(id, params);
			
		}
		
		public function clearBlock(id:String):void {
			
			log.info("clearBlock(), id = " + id);
		
			var block:Block = getDisplayedBlock(id);
			
			if (block == null) {
				log.info("There is no block to clear with id: " + id);
				return;
			}
			
			if (block.master.isCleared) {
				log.info("The block is already being cleared: " + id);
				return;
			}
			
			block.master.clear();
			
			// FIXME: Keep tracks of the blocks that are being cleared so they can be before they are completely clear.
//			removeDisplayedBlock(id);
			
			clearDepends(block);
			
		}
		
		public function getDisplayedBlock(id:String):Block {
			var block:Block = null;
			for each (var b:Block in displayedBlocks) {
				if (b.context.id == id) {
					block = b;
					break;
				}
			}
			return block;
		}
		
		public function getDisplayedBlocks():Array {
			return displayedBlocks;
		}

		public function isDisplayedBlock(id:String):Boolean {
			return getDisplayedBlock(id) != null;
		}
		
		public function getDisplayedBlockByTarget(target:Object):Block {
			var block:Block = null;
			for each (var b:Block in displayedBlocks) {
				if (b.master.target == target) {
					block = b;
					break;
				}
			}
			return block;
		}
		
		public function createPage(context:PageContext, checkConsistency:Boolean = true):void {
			factory.createPage(context, checkConsistency);
		}
		
		public function removePage(id:String, checkConsistency:Boolean = true):void {
			factory.removePage(id, checkConsistency);
		}
		
		public function containsPage(id:String):Boolean {
			return factory.containsPage(id);
		}
		
		public function getPage(id:String):PageContext {
			return factory.getPage(id);
		}
		
		public function getPages():Array {
			return factory.getPages();
		}

		public function getPagesAt(level:Number):Array {
			return factory.getPagesAt(level);
		}
		
		public function createBlock(context:BlockContext, checkConsistency:Boolean = true):void {
			factory.createBlock(context, checkConsistency);
		}
		
		public function removeBlock(id:String, checkConsistency:Boolean = true):void {
			factory.removeBlock(id, checkConsistency);
		}
		
		public function containsBlock(id:String):Boolean {
			return factory.containsBlock(id);
		}
		
		public function getBlock(id:String):BlockContext {
			return factory.getBlock(id);
		}
		
		public function getBlocks():Array {
			return factory.getBlocks();
		}
		
		public override function resolveAction(handlerContext:HandlerContext, params:Object = null):void {
			
			var isPage:Boolean = getPage(handlerContext.context.id) != null;
			var currentPage:Page;
			var displayedBlock:Block;
			if (handlerContext.action == null) {
				if (isPage) {
					currentPage = getCurrentPage();
					if (currentPage != null && currentPage.context.id == handlerContext.context.id) {
						if (currentPage.master.target != null)
							ObjectUtils.merge(currentPage.master.target, params); 
					} else {
						goto(handlerContext.context.id, params);
					}
				} else {
					displayedBlock = getDisplayedBlock(handlerContext.context.id);
					if (displayedBlock != null) {
						if (displayedBlock.master.target != null)
							ObjectUtils.merge(displayedBlock.master.target, params); 
					} else {
						displayBlock(handlerContext.context.id, params);
					}
				}
			} else if (handlerContext.action == ACTION_CALL) {
				if (isPage) {
					goto(handlerContext.context.id, params);
				} else {
					displayBlock(handlerContext.context.id, params);
				}
			} else if (handlerContext.action == ACTION_GOTO) {
				goto(handlerContext.context.id, params);
			} else if (handlerContext.action == ACTION_DISPLAY_BLOCK) {
				displayBlock(handlerContext.context.id, params);
			} else if (handlerContext.action == ACTION_CLEAR_BLOCK) {
				clearBlock(handlerContext.context.id);
			} else {
				var f:Function;

				try
				{
					if (isPage) {
						currentPage = getCurrentPage();
						if (currentPage != null && currentPage.context.id == handlerContext.context.id) {
							f = currentPage.master.target[handlerContext.action];
						} else {
							goto(handlerContext.context.id, params);
						}
					} else {
						displayedBlock = getDisplayedBlock(handlerContext.context.id);
						if (displayedBlock != null) {
							f = displayedBlock.master.target[handlerContext.action];
						} else {
							displayBlock(handlerContext.context.id, params);
						}
					}
				} catch(e:ReferenceError) {
					log.warning(INVALID_ACTION.replace("{id}", handlerContext.context.id).replace("{action}", handlerContext.action));
				}
				
				if(f != null) {
					try
					{
						f.apply(null, [params]);
					} catch(ar:ArgumentError) {
						f.apply(null, null);
					}
				} else {
					log.warning(INVALID_ACTION.replace("{id}", handlerContext.context.id).replace("{action}", handlerContext.action));							
				}
			}
		}
		
		public override function resolvePriority(handlerContext:HandlerContext):int {
			var priority:int = handlerContext.priority;
			if (priority == 0) {
				if (handlerContext.action == null || handlerContext.action == ACTION_CALL || handlerContext.action == ACTION_GOTO || handlerContext.action == ACTION_DISPLAY_BLOCK)
					priority = HANDLER_PRIORITY_HIGH;
			}
			return priority;
		}
		
		
		// PRIVATE METHODS
		
		private function displayDepends(pageBlock:AbstractPageBlock):void {
			
			log.debug("displayDepends(), id = " + getPageBlockId(pageBlock));
			
			if (pageBlock == null)
				return;
			 
			// Get the depends for the current request.
			var a:Array = getFlattenedDepends(pageBlock);
			
			// Display the block.
			for (var i:int = 0; i < a.length; i++) {
				var params:Object = new Object();
				params[PARAM_NAME_CALLER] = getPageBlockId(pageBlock);
				displayBlock(a[i], params);
			}
			
			// If the caller is a page, remove the previous page's obsolete depends.
			if (pageBlock is Page) {
				var prevPage:Page = Page(pageBlock).request.referrer;
				if (prevPage != null)
					clearDepends(prevPage);
			}
			
		}
		
		private function clearDepends(pageBlock:AbstractPageBlock):void {
			
			log.debug("clearDepends(), id = " + getPageBlockId(pageBlock));
		
			// Get the flattened depends.
			var a:Array = getFlattenedDepends(pageBlock);
				
			// Iterate the depends and remove the previous page as a caller.
			// If there's no callers left, remove the depends.
			for (var i:int = 0; i < a.length; i++) {
				var block:Block = getDisplayedBlock(a[i]);
				if (block == null)
					continue;
				block.removeCaller(getPageBlockId(pageBlock));
				if (block.getCallers().length == 0)
					clearBlock(block.context.id);
			}
			
		}
		
		private function getFlattenedDepends(o:Object, aInit:Array = null):Array {
			
			// Create the array containing depends names or set it to an initial one.
			var a:Array = aInit != null ? aInit : new Array();
			
			// Get the depends.
			var depends:Array;
			if (o is AbstractPageBlockContext) {
				depends = AbstractPageBlockContext(o).depends;
			} else if (o is AbstractPageBlock) {
				depends = AbstractPageBlock(o).getDepends();
			} else {
				return a;
			}
			
			// Iterate through all depends in this context.
			for (var i:Number = 0; i < depends.length; i++) {
				
				// Get the name.
				var id:String = depends[i];
				
				// If it's already in the array, continue.
				if (a.indexOf(id) != -1)
					continue;
				
				// Add the depends and subdepends to the array.
				a.push(id);
				var b:BlockContext = getBlock(id);
				getFlattenedDepends(b, a);
				
			}
			
			return a;
			
		}
		
		private function addDisplayedBlock(block:Block):void {
		
			log.debug("addDisplayedBlock(), id = " + block.context.id);
			
			if (getDisplayedBlock(block.context.id) == null)
				displayedBlocks.push(block);
			
		}
		
		private function removeDisplayedBlock(id:String):void {
		
			log.debug("removeDisplayedBlock(), id = " + id);
			
			var a:Array = new Array();
			for (var i:Number = 0; i < displayedBlocks.length; i++) {
				var block:Block = displayedBlocks[i];
				if (block.context.id != id)
					a.push(block);
			}
			displayedBlocks = a;
			
		}
		
		private function getPageBlockId(pageBlock:AbstractPageBlock):String {
			var id:String = null;
			if (pageBlock is Page) {
				id = Page(pageBlock).context.id;
			} else if (pageBlock is Block) {
				id = Block(pageBlock).context.id;
			}
			return id;
		}
		
		private function registerPageBlockHandlerContexts(context:AbstractPageBlockContext):void {
			for each (var handlerContext:HandlerContext in context.handlers)
				registerHandlerContext(handlerContext);
		}
		
		
		// EVENT LISTENERS
		
		private function pageContextCreatedHandler(event:PagesEvent):void {
			registerPageBlockHandlerContexts(event.context);
		}
		
		private function blockContextCreatedHandler(event:PagesEvent):void {
			registerPageBlockHandlerContexts(event.context);
		}
		
		private function pageOffHandler(event:PagesEvent):void {
			
			if (nextPage == null) {
				log.info("Skipping display the requested page at off since nextPage is null. There has probably been another page request in the meantime.");
				return;
			}
			
			log.debug("Displaying the requested page at off: " + nextPage.context.id);
			
			displayedPage = nextPage;
			nextPage = null;
			displayedPage.master.display();
			displayDepends(displayedPage);
			
		}
		
		private function blockOffHandler(event:PagesEvent):void {
			removeDisplayedBlock(event.context.id);
		}

		private function pageLoadErrorHandler(event:PagesEvent):void {
			currentPage = null;
			displayedPage = null;
		}
		
		private function targetCreatedHandler(event:PagesEvent):void {
			session.targetRegistry.registerTarget(event.context.id, event.targetObject);
		}
		
		private function clearHandler(event:PagesEvent):void {
			session.targetRegistry.unregisterTarget(event.targetObject);
		}
		
		
		// GETTER/SETTER
		
		protected function get factory():AbstractPageBlockFactory {
			return _factory;
		}
		
		protected function set factory(factory:AbstractPageBlockFactory):void {
			
			_factory = factory;
			
			var pageContexts:Array = getPages();
			for each (var pageContext:PageContext in pageContexts)
				registerPageBlockHandlerContexts(pageContext);
				
			var blockContexts:Array = getBlocks();
			for each (var blockContext:BlockContext in blockContexts)
				registerPageBlockHandlerContexts(blockContext);
			 
			_factory.addEventListener(PagesEvent.PAGE_CONTEXT_CREATED, pageContextCreatedHandler);
            _factory.addEventListener(PagesEvent.BLOCK_CONTEXT_CREATED, blockContextCreatedHandler);
            
		}
		
		
	}
	
}
